﻿<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.0/spark-md5.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
    <title>文件上传测试 localhost:60000</title>
    <meta charset="utf-8" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <script type="text/javascript">
        var baseApi = 'http://localhost:60000/api/';

        //每次上传文件的大小 字节
        var chunkSize = 1024 * 1024;
        //顶级文件夹GUID
        var topDirGuid = "";
        //兼容切片写法，实际上切片返回的是指针数据，并不是真正把文件切片了
        var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;

        //文件集合
        var fileList = [];
        var fileListDisplay = document.getElementById('file-list-display');

        window.onload = () => {
            var files = document.getElementById("files");
            files.addEventListener("click", function (event) {
                fileList = [];//清空上传控件数据
                files.value = "";//清空上传控件数据
                $("#file-list-display").html("等待上传...")//清空列表
            })
            files.addEventListener("change", function (event) {
                $("#file-list-display").html("")//清空列表
                for (var i = 0; i < files.files.length; i++) {
                    fileList.push(files.files[i]);

                    //md5生成，只用第一片文件的md5，生成完毕后上传文件
                    checkMd5ThenUpload(files.files[i])
                }
            });
        };

        /**
         * 根据文件和返回信息，对列表信息进行修改
         * @param file 完整文件信息（非分片）
         * @param maxChunk 最大分片，用来计算进度
         * @param returnInfo 后台返回的信息
         */
        function renderFileInList(file, maxChunk, returnInfo) {
            if (returnInfo != null && returnInfo.Session != "") {
                let html = "";
                switch (returnInfo.Statu) {
                    case 1: html += "会话Session:" + returnInfo.Session + " 正常上传中"; break;
                    case 2: html += "会话Session:" + returnInfo.Session + " 断点续传中"; break;
                    case 3: html += "会话Session:" + returnInfo.Session + " 上传成功" + " 文件GUID为：" + returnInfo.FileGuid; break;
                    case 4: html += "会话Session:" + returnInfo.Session + " 秒传成功" + " 文件GUID为：" + returnInfo.FileGuid; break;
                    default: html += "会话Session:" + returnInfo.Session + " 未知";
                }
                //未找到同id的dom结构，则需要新建
                if ($("#" + returnInfo.Session + "").length == 0) {
                    var fileDisplayEl = document.createElement("p");
                    fileDisplayEl.id = returnInfo.Session;//设置标识，方便回来修改状态
                    fileDisplayEl.innerHTML = html + " " + file.name;
                    $("#file-list-display").append(fileDisplayEl);
                } else {
                    $("#" + returnInfo.Session + "").html(
                        html + " " +
                        file.name +
                        " 进度" + (returnInfo.Chunk / maxChunk * 100).toFixed(5))
                }
            }

        };

        /**
         * 计算md5之后，请求上传接口（fileReader是个异步文件读取，所以需要在onload内调用上传）
         * @param file 完整文件信息（非分片）
         */
        function checkMd5ThenUpload(file) {
            let spark = new SparkMD5(),
                fileReader = new FileReader();

            fileReader.onload = function (e) {
                spark.appendBinary(e.target.result);// append array buffer
                let md5 = spark.end();// 完成计算，md5结果
                console.log("文件读取成功！MD5计算得到为：", md5);

                let path = file.webkitRelativePath.split('/')//文件目录
                path.pop();//去掉文件名
                path = path.join('/');//再拼回去
                let maxChunk = Math.ceil(file.size / chunkSize);//总区块数
                fileUpload(file, 1, "", 0, md5, path, maxChunk)//最后进行上传
            };

            fileReader.onerror = function () {
                running = false;
                console.log("文件读取失败！");
            };

            //或者用 fileReader.readAsBinaryString(blobSlice.call(file, 0, chunkSize)) 兼容性更好 注意第17行声明 blobSlice 工具实例
            fileReader.readAsBinaryString(file.slice(0, chunkSize));//使用fileReader读取第一篇文件，读取完毕后获得md5
        }

        /**
         * 上传业务
         * @param file 完整文件
         * @param index 第几个分片
         * @param session 后台返回的会话session，用于标识文件
         * @param statu 状态（第一片状态为0，递归中状态需要根据后台返回的状态做变更，最后一片状态设置为3）
         * @param md5 MD5摘要，需要第一片上传前，由前端计算第一片分片文件得到
         * @param path 文件所属子目录
         * @param maxChunk 文件总区块数
         * @param lackedChunks 断点续传的chunks集合，递归减少(仅断点续传逻辑有用)
         */
        function fileUpload(file, index, session, statu, md5, path, maxChunk, lackedChunks) {
            let formData = new FormData();
            formData.append("statu", statu);
            formData.append("file",
                file.slice((index - 1) * chunkSize, index * chunkSize)//将文件进行分段
                //或者用 blobSlice.call(file, 0, chunkSize)) 兼容性更好 注意第17行声明 blobSlice 工具实例
            );
            formData.append("topDirGuid", topDirGuid);
            formData.append("session", session);
            formData.append("name", file.name);
            formData.append("path", path);
            formData.append("chunkSize", chunkSize);
            formData.append("fileSize", file.size);
            formData.append("chunk", index);
            formData.append("maxChunk", maxChunk);
            formData.append("md5", md5);

            $.ajax({
                url: baseApi + 'FileUpload/UploadChunkFile',
                type: 'POST',
                cache: false,
                data: formData,
                processData: false,
                contentType: false,
            }).done(function (res) {
                renderFileInList(file, maxChunk, res)//列表中渲染文件信息
                if (res.Statu == 1) {//正常上传逻辑
                    index++;//接着上传下个分片
                    let statu = res.Statu;
                    if (index == maxChunk) statu = 3;//如果是最后一片，设置为3
                    console.log("正常上传中, 接下来发送分片为第", index, "片", "当前状态值为：", statu, res);
                    fileUpload(file, index, res.Session, statu, md5, path, maxChunk);
                } else if (res.Statu == 2) {//断点续传逻辑
                    if (res.LackedChunks.length > 0)
                        lackedChunks = res.LackedChunks;
                    index = lackedChunks[0];//接着上传缺失分片的第一个分片
                    lackedChunks.shift();//移除准备上传的第一片
                    let statu = res.Statu;
                    if (index == lackedChunks[lackedChunks.length - 1]) statu = 3;//如果是最后一片，设置为3，很重要！
                    console.log("断点续传中, 接下来发送分片为第", index, "片", "当前状态值为：", statu, res);
                    fileUpload(file, index, res.Session, statu, md5, path, maxChunk, lackedChunks);
                } else if (res.Statu == 3) {//上传成功提示
                    console.log("上传成功", res);
                }
                else if (res.Statu == 4) {//秒传成功提示
                    console.log("文件已存在，秒传成功", res);
                }
            }).fail(function (res) {
                console.log("上传失败", res);
            });
        }

        /**
         * 获取新的文件夹信息 
         */
        function getNewDir() {
            if (!$("#dirName").val()) {
                alert("未输入文件夹名称");
            } else {
                $.ajax({
                    url: baseApi + "FileUpload/CreateNewDir",
                    type: "post",
                    dataType: "json",
                    contentType: 'application/json',
                    data: JSON.stringify({
                        name: $("#dirName").val()
                    }),
                    success: function (data) {
                        topDirGuid = data;
                        $("#topDirGuid").html(topDirGuid)
                    },
                    error: function (data) {
                        $("#topDirGuid").html(data);
                    }
                });
            }
        }
    </script>
</head>

<body>
    <div>
        顶级文件夹名称(如果没有可以不填)：<input type="text" value="" id="dirName" />
    </div>
    <div>
        顶级文件夹GUID：<span style="width: 300px;margin-left:10px" id="topDirGuid"></span>
    </div>
    <div style="margin-top:20px;">
        <input type="button" value="设置顶级文件夹" onclick="getNewDir()" />
        <input type="file" id="files" multiple="" webkitdirectory="true" directory="true" data-sign="NORMAL_INPUT_FILE_MATE" />
    </div>
    <div style="margin-top:20px;" id='file-list-display'>
        等待上传...
    </div>
    <p style="color:red;font-size:10px">目前可以单文件上传，多文件上传，不设置顶级文件夹也可以，可以随意测试。</p>
    <p style="color:red;font-size:10px">控制台会输出上传情况，可以查看返回的信息。</p>
</body>

</html>